/**
 * simplest broadcast server (limit: 100 bytes per line)
 * move from javascript ROOT to WEB-INF/classes/wapeter/server
 */
package wapeter.server;

import java.net.*;
import java.io.*;
import java.util.*;

class Dir {
    public static final int NORTH = 1;
    public static final int WEST = 2;
    public static final int SOUTH = 3;
    public static final int EAST = 4;
}


class Arrow {

    // name, x, y
    String name = "-[bugbug]";
    int x = 0;
    int y = 0;
    int tx = -1;
    int ty = -1;
    int tcount = 0;
    final int tmax = 5; // arrow is faster
    int dir = Dir.NORTH;
    int life = 15; // every Moving, life --, if life <= 0, remove it


    Arrow(String _name, int _x, int _y, int _dir) {
        name = _name;
        x = _x;
        y = _y;
        tcount = 0;
        dir = _dir;
        tx = x + Board.dir2dx(dir);
        ty = y + Board.dir2dy(dir);
        life = Math.max(Board.map[0].length, Board.map.length) * tmax;
        // no need to init tmax
    }
    

    /**
     * need input targetX, Y, because it may NOT always movable
     * just turn the face.
     */
    public int pos2dir(int targetX, int targetY) {
        // from x, y, tx, ty return the correct direction

        int d; // new direction
        int dx, dy, adx, ady;
        dx = targetX - x;
        dy = targetY - y;
        adx = Math.abs(dx);
        ady = Math.abs(dy);

        d = 1 + (dy + 1) * ady + (dx + 2) * adx;
        assert(d >= 1 && d <= 4);

        return d;
    }


    /**
     * only position, seperated by :
     * @return
     */
    public String toPosString() {
        return x + ":" + y + ":" + tx + ":" + ty + ":" + tcount + ":" + tmax + ":" + dir;
    }
    
    public String toString() {
        return name + ":" + toPosString();
    }
    
}


class Player {
    // name, x, y
    String name = "[bug]";
    int x = 0;
    int y = 0;
    int tx = -1;
    int ty = -1;
    int tcount = -1;
    final int tmax = 10;  // player is slow, arrow is fast
    int dir = Dir.NORTH;
    int nextDir = -1;


    Player(String _name, int _x, int _y, int _dir) { 
        name = _name;
        x = _x;
        y = _y;
        tx = -1;
        ty = -1;
        tcount = 0;
        dir = _dir;
    }


    /**
     * need input targetX, Y, because it may NOT always movable
     * just turn the face.
     */
    public int pos2dir(int targetX, int targetY) {
        // from x, y, tx, ty return the correct direction

        int d; // new direction
        int dx, dy, adx, ady;
        dx = targetX - x;
        dy = targetY - y;
        adx = Math.abs(dx);
        ady = Math.abs(dy);

        d = 1 + (dy + 1) * ady + (dx + 2) * adx;
        assert(d >= 1 && d <= 4);

        return d;
    }


    /**
     * only position, seperated by :
     * @return
     */
    public String toPosString() {
        return x + ":" + y + ":" + tx + ":" + ty + ":" + tcount + ":" + tmax + ":" + dir;
    }


    public String toString() {
        return name + ":" + toPosString();
    }
}


// board of map
class Board {
    // assume every row has same col 
    // [y][x] OR [row][col]
    public final static int map[][] = {
        {0, 0, 0, 1, 0, 0, 0, 0, 2, 0}, 
        {0, 0, 0, 0, 2, 0, 0, 0, 0, 0},
        {2, 0, 0, 0, 0, 0, 0, 0, 1, 0},
        {0, 0, 0, 1, 0, 0, 1, 0, 0, 0},
        {0, 0, 2, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 0, 0, 0, 0, 2, 0, 0},
        {0, 0, 0, 0, 0, 1, 0, 1, 0, 0},
        {0, 0, 0, 2, 0, 0, 0, 0, 0, 0}
    };

    // public final static String player[] = {
    //     "peter", "hui", "cindy", "xiaoyu"
    // };

    public final static Player playerList[] = {
        new Player("peter", 0, 0, Dir.EAST),
        new Player("hui", map[0].length - 1, 0, Dir.WEST), 
        new Player("cindy", 0, map.length - 1, Dir.NORTH),
        new Player("xiaoyu", map[0].length - 1, map.length - 1, Dir.NORTH),
        new Player("sisi", 2, 2, Dir.SOUTH),
    };

    public final static ArrayList<Arrow> arrowList = new ArrayList<Arrow>();

    public final static int ROW = map.length;
    public final static int COL = map[0].length;

    public static Arrow getArrow(String name) {
        if (null == name) {
            return null;
        }

        Iterator<Arrow> it = arrowList.iterator();
        while (it.hasNext()) {
            Arrow a = it.next();
            if (name.equals(a.name)) {
                return a;
            }
        }

        // this is dangerous, as arrowList may be modified in Moving thread
        /* keep this for reference
        for (Arrow a : arrowList) {
            if (name.equals(a.name)) {
                return a;
            }
        }
        */
        return null;
    }


    /**
     * 
     * @param name
     * @return the player with name matched, null if not found
     */
    public static Player getPlayer(String name) {
        if (null == name) {
            return null;
        }
        for (Player p : playerList) {
            if (name.equals(p.name)) {
                return p;
            }
        }
        return null;
    }


    public static boolean valid(int x, int y) {
        if (y < 0 || y >= map.length) {
            return false;
        }
        if (x < 0 || x >= map[0].length) {
            return false;
        }
        return true;
    }


    public static int dir2dx(int dir) {
        return (dir - 3) * (1 ^ (dir & 1));
    }


    public static int dir2dy(int dir) {
        return (dir - 2) * (dir & 1);    
    }


    public static int move(String name, String data) {
        String[] list = data.split(":");
        // System.out.println("list.length=" + list.length);
        // System.out.println("list[0]=" + list[0]);
        // System.out.println("list[1]=" + list[1]);

        // change to North East South West style
        if (list.length < 1) {
            System.err.println("ERROR Board.move wrong data: " + data);
            return -1;
        }

        int dir = -1;
        try {
            dir = Integer.parseInt(list[0].trim());
        } catch (Exception e) {
            System.err.println("ERROR Board.move dir parseInt ex: " + e);
            return -1;
        }

        // dir == 0 means shoot
        if(dir < 0 || dir > 4){
            System.err.println("ERROR Board.move dir =" + dir);
            return -1;
        }

        Player player;
        player = getPlayer(name);
        if (null == player) {
            System.err.println("ERROR Board.move player not found: " + name);
            return -3;
        }

        if(player.tx > 0 && player.tcount >= (player.tmax/2)){
            player.nextDir = dir;
            return -2;
        }

        
        // shoot, click my player
        if (dir == 0){
            String owner = "-" + player.name;
            Arrow a;
            a = getArrow(owner);
            if (a != null) {
                System.err.println("WARN duplicate arrow");
                return -2;
            }
            a = new Arrow(owner, player.x, player.y, player.dir);
            arrowList.add(a);
            System.out.println("Board.move: shoot arrow: " + a.toString());
            return 0;
        }
        
        int x, y;
        x = player.x + dir2dx(dir);
        y = player.y + dir2dy(dir);
        if(valid(x, y) == false){
            System.err.println("ERROR Board move out of map : " + x + ", " + y);
            return -2;
        }


        // avoid cover other players
        for (int i = 0; i < playerList.length; i++){
            if(playerList[i].x == x && playerList[i].y == y){
                player.dir = dir;
                System.out.println("WARN cannot move other player: " + player.dir);
                return 0;
            }
        }


        // check stone
        if(map[y][x] > 0){
            player.dir = dir;
            System.out.println("WARN cannot move stone, change dir: " + player.dir);
            return 0;
        }

        // data split 
        // playerList[i].x = x;
        // playerList[i].y = y;
        if (player.tx != -1) {
            System.out.println("WARN cannot move, moving");
            return -16;
        }
        player.tx = x;
        player.ty = y;
        player.tcount = 0; // for debug=3,  normal=0
        player.dir = dir;

        return 0;
    }


    public static int send(Socket sock) {
        String str;
 
        try {
            OutputStream out = sock.getOutputStream();

            // first send col and row \n
            // send row line by line with \n

            str = "" + Integer.toString(COL) + " " + Integer.toString(ROW) 
                + " " + playerList.length + " " + arrowList.size();
            // out.write(str.getBytes());

            for (int r = 0; r < ROW; r++) {
                str += "|";
                for (int c = 0; c < COL; c++) {
                    str += "" + map[r][c] + " ";
                }
                // out.write(str.getBytes());
            }

            for (int i = 0; i < playerList.length; i++) {
                Player p = playerList[i];
                str += "|";
                // str += p.name + " " + p.x + " " + p.y + "|";
                str += p.toString();
            }

            for (Arrow a : arrowList) {
                str += "|";
                str += a.toString();
            }


            // str += "|"; // actually nothing after this
            // javascript console.log print all player's name
            str += "\n";

            out.write(str.getBytes());
            out.flush();
            System.out.println("DONE sendMap");

        } catch (Exception e) {
            System.err.println("ERROR Board.send ex: " + e);
            return -1;
        }

        return 0;
    }

}


/**
 * for user moving
 */
class Moving extends Thread {
    Broadcaster bc;

    public Moving(Broadcaster _bc) {
        bc = _bc;
    }

    boolean running = true;
    @Override
    public void run() {
        while (running) {
            try {
                sleep(20); // slower
            } catch (Exception e) {
                System.err.println("sleep ex: " + e);
                break;
            }

            for (Player p : Board.playerList) {
                if (p.tx == -1) {
                    continue; // skip non-moving player
                }

                p.tcount++;
                // ending criteria
                if (p.tcount >= p.tmax) {
                    p.x = p.tx;
                    p.y = p.ty;
                    p.tx = -1;
                    p.ty = -1;
                    p.tcount = 0;
                }

                byte list[];
                list = (p.toString() + "|").getBytes();

                // such that two or more lists are sent in a very short time.
                // System.out.println("Moving: player: " + (new String(list)));
                bc.sendAll(list, 0, list.length);
            }

            // loop for arrow, it must use iterator,
            // as we may remove arrow inside the loop.
            Iterator<Arrow> it = Board.arrowList.iterator();
            while (it.hasNext()) {
                Arrow a = it.next();
                // safety
                a.life--;
                if (a.life <= 0) {
                    it.remove();
                    a.x = -1;
                    a.y = -1;
                    byte list[] = (a.toString() + "|").getBytes();
                    bc.sendAll(list, 0, list.length);
                    System.out.println("WARN Moving: no life arrow: " + (new String(list)));
                    continue;
                }
                a.tcount++;
                if (a.tcount >= a.tmax) {
                    a.x = a.tx;
                    a.y = a.ty;
                    a.tcount = 0;
                    a.tx = a.x + Board.dir2dx(a.dir);
                    a.ty = a.y + Board.dir2dy(a.dir);
                    // System.out.println("Moving arrow next grid : " + a.toPosString());
                    String owner = a.name.substring(1);
                    // System.out.println("owner=" + owner + "ax,y: " + a.x + ", " + a.y);

                    boolean hit = false;
                    for (Player p : Board.playerList) {
                        if (owner.equals(p.name)) {
                            continue; // skip owner, cannot kill myself
                        }
                        // hit check!
                        if (p.x == a.x && p.y == a.y) {
                            // instant kill now, TODO life and flash when dying
                            p.x = -1; 
                            p.y = -1;
                            a.x = -1;
                            a.y = -1;
                            hit = true;
                            byte list[] = (p.toString() + "|" + a.toString() + "|").getBytes();
                            System.out.println("Moving HIT : " + (new String(list)));
                            bc.sendAll(list, 0, list.length);
                            it.remove(); // remove this arrow
                            break;
                        }
                    }
                    if (hit) {
                        continue; // skip this loop
                    }

                    // order is important, after hit check
                    // out of bound OR hit the hill, remove
                    if (false == Board.valid(a.x, a.y) || Board.map[a.y][a.x]==2) {
                        a.x = -1; // x=-1 is enough to indicate remove arrow
                        a.y = -1;
                        byte list[] = (a.toString() + "|").getBytes();
                        bc.sendAll(list, 0, list.length);
                        // System.out.println("Moving arrow remove: " + (new String(list)));
                        it.remove(); // special, remove this arrow
                        continue;  // early continue
                    }
                }
                // no remove
                byte list[] = (a.toString() + "|").getBytes();
                bc.sendAll(list, 0, list.length);
                // System.out.println("Moving: arrow: " + (new String(list)));

            }


            // nextDir specific
            // data is adjusted, but not sent to client
            for (final Player p : Board.playerList) {
                if (p.nextDir >= 0 && p.tx < 0) {
                    Board.move(p.name, "" + p.nextDir);
                    p.nextDir = -1;
                }
            }

        }

    }

}


class Login extends Thread {
    Socket sock;
    Broadcaster bc;

    // List is immutable, ArrayList is mutable
    // for real server, user list is mutable, 
    // this is simple version, make it immutable
    // ArrayList<String> userList = Arrays.asList("peter", "hui", "cindy");

    Login(Socket _sock, Broadcaster _bc) {
        sock = _sock;
        bc = _bc;

    }

    @Override
    public void run() {
        InputStream in;
        OutputStream out;
        byte [] byteList = new byte[100]; // enough for user name
        int n;
        String user;

        try {
            in = sock.getInputStream();
            out = sock.getOutputStream();
        } catch (Exception e) {
            System.err.println("Login.run: " + e);
            return; // early exit
        }

        try {
            n = in.read(byteList);
            if (n <= 0) {
                out.write("ERROR user name is empty\n".getBytes());
                out.flush(); // make sure error message is completely sent
                sock.close();
                return;
            }

            // trim = 把字符串后面的空白,换行等等去掉
            user = (new String(byteList, 0, n)).trim();
            System.out.println("INPUT name=[" + user + "]");

            for (int i = 0; i < Board.playerList.length; i++) {
                //  ==  不行, 因为指针不一样
                if (user.equals(Board.playerList[i].name)) {
                    // good, it is a valid user
                    System.out.println("User logging-in: " + user);
                    Board.send(sock);  // send the board to client
                    bc.add(sock, user);
                    // do NOT close the socket
                    return; // early exit, stop this thread
                }
            }
            // implicit: user not found
            String str = "ERROR user not found: " + user + "\n";
            System.err.print(str);
            out.write(str.getBytes());
            out.flush();
            sock.close();
            
        } catch (Exception e) {
            System.err.println("ERROR Login.run ex: " + e);
        }

        System.out.println("Login thread stop");

    }
}


class Conn extends Thread {
    Socket sock;
    InputStream in;
    OutputStream out;
    Broadcaster bc;
    String user = "_BUG_"; 

    Conn(Socket _sock, Broadcaster _bc, String _user) {
        sock = _sock;
        bc = _bc;
        user = _user;
        try {
            in = sock.getInputStream();
            out = sock.getOutputStream();
        } catch (Exception e) {
            System.err.println("Conn.Conn ex: " + e);
        }
        
    }

    public void close() {
        try {
            if (in != null) {
                in.close();
                in = null;
            }
        } catch (Exception e) {
            System.err.println("Conn.close in : " + e);
        }
        try {
            if (out != null) {
                out.close();
                out = null;
            }
        } catch (Exception e) {
            System.err.println("Conn.close out : " + e);
        }
        try {
            if (sock != null) {
                sock.close();
                sock = null;
            }
        } catch (Exception e) {
            System.err.println("Conn.close sock : " + e);
        }
        sock = null;
        in = null;
        out = null; // for gc
    }

    @Override
    public void run() {
        byte[] byteList = new byte[100];
        int n = 0;
        int success = 0;
        int ret = 0;
        final byte[] quit = "quit".getBytes();
        final byte [] userByte = (user + ":").getBytes();


        try {
            while ((n = in.read(byteList)) > 0) {
                // 这里不用抄
                if (n >= quit.length) {
                    final byte[] cmd = Arrays.copyOf(byteList, quit.length);
                    if (Arrays.equals(cmd, quit)) {
                        System.out.println("quit cmd received, interrupt");
                        bc.interrupt();
                        break; // quit this connection also
                    }
                }

                String data = new String(byteList, 0, n);
            
                ret = Board.move(user, data);
                if (ret != 0) {
                    continue; // no sendAll, cannot move OR error
                }
                // create a message: 
                // player.x:player.y:player.tx:player.tx:player.tcount:player.tmax
                Player player = Board.getPlayer(user);
                String str = user + ":" + player.toPosString() + "|";
                // System.out.println("Before sendAll: " + str);

                byte[] list = str.getBytes();
                // byte[] list = new byte[userByte.length + posString.length()];
                // arraycopy( 来源数组, 来源开始位置, 目标数组, 目标开始位置, 长度)
                // System.arraycopy(userByte, 0, list, 0, userByte.length);
                // System.arraycopy(byteList, 0, list, userByte.length, );

                success = bc.sendAll(list, 0, list.length);
                System.out.println("SERVER: Success sendAll = " + success);
            }
        } catch (Exception e) {
            System.err.println("Conn.run ex: " + e);
            e.printStackTrace();
        }
        close(); // Conn.close
    }

    @Override  
    public void interrupt(){  
        try {
            close();
        } catch (Exception e) {
            System.err.println("Conn.interrupt close ex: " + e);
        } finally {
            super.interrupt();  
        }

    }   


    public String toString() {
        InetAddress address = sock.getInetAddress();
        return "Conn:" + address.toString();
    }

}


class Broadcaster extends Thread {

    boolean running = true;
    ServerSocket sssSock = null;

    ArrayList<Conn> connList = new ArrayList<Conn>();

    Broadcaster(ServerSocket _sssSock) {
        sssSock = _sssSock;
    }

    public void add(Socket sock, String user) {
        Conn conn = null;
        Iterator<Conn> it = connList.iterator();
        while (it.hasNext()) {
            conn = it.next();
            if (user.equals(conn.user)) {
                conn.close(); // close the connection
                it.remove();
            }
        }


        try {
            conn = new Conn(sock, this, user);
        } catch (Exception e) {
            System.err.println("add sock ex: " + e);
            return;
        }

        connList.add(conn);
        conn.start(); // start the thread
    }

    public void closeAll() {
        Iterator<Conn> it = connList.iterator();

        while (it.hasNext()) {
            Conn conn = it.next();
            conn.close();
            it.remove();
        }

    }


    /**
     * 
     * @param byteList
     * @param offset
     * @param count
     * @return number of successful send
     */
    public int sendAll(byte[] byteList, int offset, int count) {
        int success = 0;
        Iterator<Conn> it = connList.iterator();

        while (it.hasNext()) {
            Conn c = it.next();
            try {
                if (null == c.out) {
                    c.close();
                    it.remove();
                    continue;
                }
                c.out.write(byteList, offset, count);
                success ++;
            } catch (Exception e) {
                // remove this connection
                System.err.println("sendAll ex: " + e); 
                c.close(); // safe close
                it.remove();
            }

        }

        return success;
    }

    public void run() {
        Socket cSock = null;
        Login login;
        while (running) {
            try {
                cSock = sssSock.accept();
                System.out.println("accept a socket");
                // add(cSock, "peter");
                login = new Login(cSock, this);
                login.start(); 
            } catch (Exception e) {
                System.err.println("Broadcaster.run ex: " + e);
                break;
            }

        }

        try {
            sssSock.close();
        } catch (Exception e) {
            System.err.println("sssSock.close ex:" + e);
        }

    }

    @Override  
    public void interrupt(){  
        try {
            sssSock.close();
            closeAll();
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("Broadcast.interrupt close ex: " + e);
        } finally {
            super.interrupt();  
        }

    }   

}


public class Server
{

    // try static initializer
    static {
        // TODO pass IP and port ?
        String [] arg = null;
        System.out.println("wapeter.server.Server static init");
        // String [] arg = {"127.0.0.1", "7749"};
        try {
            Server.main(arg);
        } catch (Exception e) {
            System.err.println("wapeter.server.Server start ex: " + e);
        }
    }


    static void stdinLoop(Broadcaster bc) {
        String cmdQuit = "quit";

        Scanner scanner = new Scanner(System.in);
        while (true) {
            String token = scanner.next();
            if (cmdQuit.equals(token)) {
                System.out.println("CMD quit");
                bc.interrupt();
                break;
            }
        }
        scanner.close();
    }


    static Broadcaster bc = null; 

    public static void main(String arg[]) throws Exception
    {
        if (null != arg) {
            // only start with static initialization
            return;
        }
        System.out.println("----- Server start v7 -----");
        ServerSocket sssSock = null;

        // java.net.ServerSocket 

        try {
            InetAddress address = InetAddress.getByName("127.0.0.1"); 
            // 127.0.0.1 IP = a local IP address, external party cannot connect to it.
            // InetAddress address = InetAddress.getByName("127.0.0.1"); // 0.0.0.0");
            sssSock = new ServerSocket(7749, 10, address);
        } catch (Exception e) {
            System.err.println("new ServerSocket: e=" + e);
            if (sssSock != null) {
                sssSock.close();
            }
            return;
        }

        // assume OK

        bc = new Broadcaster(sssSock);
        bc.start();
        Moving m = new Moving(bc);
        m.start(); // start the moving thread
        // stdinLoop(bc);


    }
}
